跳到主要内容

Java时间处理相关学习

Date 类

使用 Date 类的默认无参构造方法创建出的对象就代表当前时间,直接输出 Date 对象显示当前的时间

Date date = new Date();
//实际上这个空参构造底层也是调用System.currentTimeMillis()

public Date() {
this(System.currentTimeMillis());
}

DateFormat

这个 DateFormat 是一个抽象类,里面包含了很多用来格式化时间的工具类

它的作用就是格式化:日期-->文本解析文本-->日期 它就包含两个成员方法:

// 将日期转换为指定格式的文本
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
// ...
}


// 将文本转换为日期
@Override
public Date parse(String source, ParsePosition pos) {
// ...
}

SimpleDateFormat

如名字所示,这个工具类使用起来是最简单的,它也是这个抽象类最基本的实现类(也只有它)

yyyy 表示四位年, MM 表示两位月份, dd 表示两位日期, HH 表示小时(使用24小时制), mm 表示分钟, ss 表示秒,这样就指定了转换的目标格式

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

注意,这个 SimpleDateFormat 不是线程安全的,所以禁止使用 static 修饰它

package com.date;
import java.util.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;

public class Inital {

public static void main(String[] args) throws ParseException {
Date date = new Date();
System.out.println(date);

// 创建SimpleDateFormat对象,指定格式的文本
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 调用format()方法,将日期转换为字符串并输出
System.out.println(sdf1.format(date));
System.out.println(sdf2.format(date));
System.out.println(sdf3.format(date));

// 使用parse()方法将文本转换为日期
String d = "2008-08-08 08:08:08";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date2 = sdf.parse(d);
System.out.println(date2);

/*
* 运行结果:
* Fri Jul 28 11:37:12 CST 2017
* 2017年07月28日 11时37分12秒
* 2017/07/28 11:37:12
* 2017-07-28 11:37:12
* Fri Aug 08 08:08:08 CST 2008
*/
}

}

SimpleDateFormat 线程不安全

先看看《阿里巴巴开发手册》对于 SimpleDateFormat 是怎么看待的:

为什么它线程不安全?

点开它的源码:

通过查看源码发现,原来 SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,例如 sdf.parse(dateStr)sdf.format(date) 诸如此类的方法参数传入的日期相关 String、Date 等等,都是交由 Calendar 引用来储存的。

这样就会导致一个问题,如果你的 SimpleDateFormat 是个 static 的,那么多个 thread 之间就会共享这个 SimpleDateFormat,同时也是共享这个 Calendar 引用。

单例、多线程、又有成员变量(这个变量在方法中是可以修改的),所以在高并发的情况下,容易出现幻读成员变量的现象,故说 SimpleDateFormat 是线程不安全的对象。

解决办法,使用 ThreadLocal,让每个线程都有自己的 SimpleDateFormat

private static final ThreadLocal<SimpleDateFormat> threadLocal = 
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

// ....

// 在线程中使用就像这样
SimpleDateFormat simpleDateFormat = threadLocal.get();

YYYY 和 yyyy

每到过年前的一两天就会集中爆发这个问题,可谓 “年经” 问题了

jDK6 的 SimpleDateFormat 只有小 “y”,没有大 “Y”。JDK7开始引入了大 “Y”,表示 Week year。

Week year 意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算入下一年。

例如 本周(2017年12月31日-2018年1月6日)跨年了。2017年12月31日 就进入了下一年。

使用示例:

 public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
calendar.set(2017, Calendar.DECEMBER, 31);

Date strDate1 = calendar.getTime();

SimpleDateFormat sf1 = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("大写 YYYY: " + sf1.format(strDate1));
SimpleDateFormat sf2 = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("小写 yyyy: " + sf2.format(strDate1));

}
// 输出结果:
// 大写 YYYY: 2018-12-31
// 小写 yyyy: 2017-12-31

所以一般用 "yyyy-MM-dd HH:mm:ss" 就够了,不要使用 YYYY

Calendar 日期时间处理

这个 Calendar 类主要进行日期时间处理

Date 类最主要的作用就是获得当前时间,同时这个类里面也具有设置时间以及一些其他的功能,但是由于本身设计的问题,这些方法却遭到众多批评,不建议使用,更推荐使用 Calendar 类进行时间和日期的处理。

java.util.Calendar 类本身是一个抽象类,但是可以通过调用 getInstance() 静态方法获取一个 Calendar 对象,此对象已由当前日期时间初始化,即默认代表当前时间

Calendar c = Calendar.getInstance();

Calendar 获取时间

package com.calender;
import java.util.Calendar;
import java.util.Date;
import java.text.SimpleDateFormat;

public class Inital {

public static void main(String[] args) {
// TODO Auto-generated method stub
Calendar ca = Calendar.getInstance();
int year = ca.get(Calendar.YEAR);//获取年份
int month = ca.get(Calendar.MONTH)+1;//获取月份,0表示1月分
int day = ca.get(Calendar.DAY_OF_MONTH);//获取日期
int hour = ca.get(Calendar.HOUR_OF_DAY);//获取小时
int minute = ca.get(Calendar.MINUTE);//获取分钟
int second = ca.get(Calendar.SECOND);//获取秒

System.out.println("当前时间为:" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second);

//将Calendar对象转换为Date对象
Date date = ca.getTime();
// 创建SimpleDateFormat对象,指定目标格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 将日期转换为指定格式的字符串
String now = sdf.format(date);
System.out.println(now);

//获取当前时间得毫秒数
long time = ca.getTimeInMillis();
System.out.println(time);
}

}

获取某月的第一天和最后一天

public class CalendarDemo {
public static void main(String[] args) {
System.out.println(getFirstDayOfMonth(2019, 2));
System.out.println(getLastDayOfMonth(2019, 2));

System.out.println(getFirstDayOfMonth(2019, 3));
System.out.println(getLastDayOfMonth(2019, 3));
}

public static String getLastDayOfMonth(int year, int month) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DATE));

return new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime());
}

public static String getFirstDayOfMonth(int year, int month) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, calendar.getMinimum(Calendar.DATE));

return new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime());
}
}

Calendar 设置时间

Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.FEBRUARY, 21, 23, 59, 59);

System.out.println(calendar.getTime());

或像上面那样单独设置

Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, 2019);
calendar.set(Calendar.MONTH, Calendar.FEBRUARY);
calendar.set(Calendar.DAY_OF_MONTH, 21);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);

System.out.println(calendar.getTime());

增加月份,如下所示:

Calendar calendar = Calendar.getInstance();

calendar.set(2019, Calendar.JANUARY, 31);
System.out.println(calendar.getTime());
calendar.add(Calendar.MONTH, 1);
System.out.println(calendar.getTime());
calendar.add(Calendar.MONTH, 2);
System.out.println(calendar.getTime());

Java8 后提供的新时间工具

在 Java8 以前,或许:

  • 当你在做有关时间日期的操作时,你会想到用 Date;
  • 当你在做日期、月份、天数相加减时,你会想到用 Calendar;
  • 当你需要对时间日期进行格式化时,你会想到使用 SimpleDateFormat 或 DateFormat 下的其他子类;
  • ……

但是,你必须知道,以上有关的时间日期操作对象,都是可变的、线程不安全的,同时,如果作为一个经常写过类似代码的人来说,尽管有相关对象提供某些操作,但并不能很快、很简单的就能得到最终想要的结果,如:要计算两个时间点之间相差的年、月、日、周、时、分、秒等,这些计算尽管原有API也能够实现,但原有API除了线程不安全之外,另外一个不足之处就是代码繁琐,性能低!

Java8 出的新的时间日期 API都是线程安全的,并且性能更好,代码更简洁!

新时间日期重要对象介绍

  • ZoneId:时区 ID,用来确定 Instant 和 LocalDateTime 互相转换的规则
  • Instant:用来表示时间线上的一个点(瞬时)
  • LocalDate:表示没有时区的日期, LocalDate是不可变并且线程安全的
  • LocalTime:表示没有时区的时间, LocalTime是不可变并且线程安全的
  • LocalDateTime:表示没有时区的日期时间, LocalDateTime 是不可变并且线程安全的
  • Clock:用于访问当前时刻、日期、时间,用到时区
  • Duration:用秒和纳秒表示时间的数量(长短),用于计算两个日期的“时间”间隔
  • Period:用于计算两个 “日期” 间隔

其中,LocalDate、LocalTime、LocalDateTime 是新 API 里的基础对象,绝大多数操作都是围绕这几个对象来进行的,有必要搞清楚:

LocalDate:只含年月日的日期对象 LocalTime:只含时分秒的时间对象 LocalDateTime:同时含有年月日时分秒的日期对象

获取当前时间

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();


System.out.println(localDate);
System.out.println(localTime);
System.out.println(localDateTime);

输出结果为:

2021-04-26
20:29:15.923725800
2021-04-26T20:29:15.923725800

根据指定日期/时间创建对象

LocalDate localDate = LocalDate.of(2021, 1, 13);
LocalTime localTime = LocalTime.of(9, 43, 20);
LocalDateTime localDateTime = LocalDateTime.of(2021, 1, 13, 9, 43, 20);

System.out.println(localDate);
System.out.println(localTime);
System.out.println(localDateTime);

运行结果:

2021-01-13
09:43:20
2021-01-13T09:43:20

日期时间的加减

对于 LocalDate,只有精度大于或等于日的加减,如年、月、日; 对于 LocalTime,只有精度小于或等于时的加减,如时、分、秒、纳秒; 对于 LocalDateTime,则可以进行任意精度的时间相加减;

LocalDateTime localDateTime = LocalDateTime.now();
//以下方法的参数都是long型,返回值都是LocalDateTime
LocalDateTime plusYearsResult = localDateTime.plusYears(2L);
LocalDateTime plusMonthsResult = localDateTime.plusMonths(3L);
LocalDateTime plusDaysResult = localDateTime.plusDays(7L);
LocalDateTime plusHoursResult = localDateTime.plusHours(2L);
LocalDateTime plusMinutesResult = localDateTime.plusMinutes(10L);
LocalDateTime plusSecondsResult = localDateTime.plusSeconds(10L);

System.out.println("当前时间是 : " + localDateTime + "\n"
+ "当前时间加2年后为 : " + plusYearsResult + "\n"
+ "当前时间加3个月后为 : " + plusMonthsResult + "\n"
+ "当前时间加7日后为 : " + plusDaysResult + "\n"
+ "当前时间加2小时后为 : " + plusHoursResult + "\n"
+ "当前时间加10分钟后为 : " + plusMinutesResult + "\n"
+ "当前时间加10秒后为 : " + plusSecondsResult + "\n"
);

//也可以以另一种方式来相加减日期,即plus(long amountToAdd, TemporalUnit unit)
// 参数1 : 相加的数量, 参数2 : 相加的单位
LocalDateTime nextMonth = localDateTime.plus(1, ChronoUnit.MONTHS);
LocalDateTime nextYear = localDateTime.plus(1, ChronoUnit.YEARS);
LocalDateTime nextWeek = localDateTime.plus(1, ChronoUnit.WEEKS);

System.out.println("now : " + localDateTime + "\n"
+ "nextYear : " + nextYear + "\n"
+ "nextMonth : " + nextMonth + "\n"
+ "nextWeek :" + nextWeek + "\n"
);

//日期的减法用法一样,在此不再举例

运行结果:

当前时间是 : 2021-04-26T21:01:35.719702500
当前时间加2年后为 : 2023-04-26T21:01:35.719702500
当前时间加3个月后为 : 2021-07-26T21:01:35.719702500
当前时间加7日后为 : 2021-05-03T21:01:35.719702500
当前时间加2小时后为 : 2021-04-26T23:01:35.719702500
当前时间加10分钟后为 : 2021-04-26T21:11:35.719702500
当前时间加10秒后为 : 2021-04-26T21:01:45.719702500

now : 2021-04-26T21:01:35.719702500
nextYear : 2022-04-26T21:01:35.719702500
nextMonth : 2021-05-26T21:01:35.719702500
nextWeek :2021-05-03T21:01:35.719702500

将年月日等修改为指定的值

其效果与时间日期相加减差不多,如今天是2018-01-13,要想变为2018-01-20有两种方式

a. localDate.plusDays(20L) -> 相加指定的天数
b. localDate.withDayOfYear(20) -> 直接指定到哪一天
LocalDate localDate = LocalDate.now();
//当前时间基础上,指定本年当中的第几天,取值范围为1-365,366
LocalDate withDayOfYearResult = localDate.withDayOfYear(200);
//当前时间基础上,指定本月当中的第几天,取值范围为1-29,30,31
LocalDate withDayOfMonthResult = localDate.withDayOfMonth(5);
//当前时间基础上,直接指定年份
LocalDate withYearResult = localDate.withYear(2017);
//当前时间基础上,直接指定月份
LocalDate withMonthResult = localDate.withMonth(5);
System.out.println("当前时间是 : " + localDate + "\n"
+ "指定本年当中的第200天 : " + withDayOfYearResult + "\n"
+ "指定本月当中的第5天 : " + withDayOfMonthResult + "\n"
+ "直接指定年份为2017 : " + withYearResult + "\n"
+ "直接指定月份为5月 : " + withMonthResult + "\n"
);

运行结果:

当前时间是 : 2021-04-26
指定本年当中的第200天 : 2021-07-19
指定本月当中的第5天 : 2021-04-05
直接指定年份为2017 : 2017-04-26
直接指定月份为5月 : 2021-05-26

获取日期的年月日周时分秒

LocalDateTime localDateTime = LocalDateTime.now();
int dayOfYear = localDateTime.getDayOfYear();
int dayOfMonth = localDateTime.getDayOfMonth();
DayOfWeek dayOfWeek = localDateTime.getDayOfWeek();
System.out.println("今天是" + localDateTime + "\n"
+ "本年当中第" + dayOfYear + "天" + "\n"
+ "本月当中第" + dayOfMonth + "天" + "\n"
+ "本周中星期" + dayOfWeek.getValue() + "-即" + dayOfWeek + "\n");

//获取当天时间的年月日时分秒
int year = localDateTime.getYear();
Month month = localDateTime.getMonth();
int day = localDateTime.getDayOfMonth();
int hour = localDateTime.getHour();
int minute = localDateTime.getMinute();
int second = localDateTime.getSecond();
System.out.println("今天是" + localDateTime + "\n"
+ "年 : " + year + "\n"
+ "月 : " + month.getValue() + "-即 "+ month + "\n"
+ "日 : " + day + "\n"
+ "时 : " + hour + "\n"
+ "分 : " + minute + "\n"
+ "秒 : " + second + "\n"
);

运行结果:

今天是2021-04-26T21:04:39.501103700
本年当中第116天
本月当中第26天
本周中星期1-即MONDAY

今天是2021-04-26T21:04:39.501103700
年 : 2021
月 : 4-即 APRIL
日 : 26
时 : 21
分 : 4
秒 : 39

时间日期前后的比较与判断

//判断两个时间点的前后
LocalDate localDate1 = LocalDate.of(2017, 8, 8);
LocalDate localDate2 = LocalDate.of(2018, 8, 8);
boolean date1IsBeforeDate2 = localDate1.isBefore(localDate2);
System.out.println("date1IsBeforeDate2 : " + date1IsBeforeDate2);

// date1IsBeforeDate2 == true

判断是否为闰年

LocalDate now = LocalDate.now();
System.out.println("now : " + now + ", is leap year ? " + );

java8时钟 : clock()

//返回当前时间,根据系统时间和UTC
Clock clock = Clock.systemUTC();

// 运行结果: SystemClock[Z]
System.out.println(clock);

时间戳

事实上 Instant 就是 Java8 以前的 Date,可以使用以下两个类中的方法在这两个类型之间进行转换,比如 Date.from(Instant) 就是用来把 Instant 转换成 java.util.date 的,而 new Date().toInstant() 就是将 Date 转换成 Instant 的

Instant instant = Instant.now();
//2019-06-08T16:50:19.174Z
System.out.println(instant);
Date date = Date.from(instant);
Instant instant2 = date.toInstant();
//Sun Jun 09 00:50:19 CST 2019
System.out.println(date);
//2019-06-08T16:50:19.174Z
System.out.println(instant2);

计算时间、日期间隔

Duration:用于计算两个“时间”间隔 Period:用于计算两个“日期”间隔

//计算两个日期的日期间隔-年月日
LocalDate date1 = LocalDate.of(2018, 2, 13);
LocalDate date2 = LocalDate.of(2017, 3, 12);
//内部是用date2-date1,所以得到的结果是负数
Period period = Period.between(date1, date2);
System.out.println("相差年数 : " + period.getYears());
System.out.println("相差月数 : " + period.getMonths());
System.out.println("相差日数 : " + period.getDays());
//还可以这样获取相差的年月日
System.out.println("-------------------------------");
long years = period.get(ChronoUnit.YEARS);
long months = period.get(ChronoUnit.MONTHS);
long days = period.get(ChronoUnit.DAYS);
System.out.println("相差的年月日分别为 : " + years + "," + months + "," + days);
//注意,当获取两个日期的间隔时,并不是单纯的年月日对应的数字相加减,而是会先算出具体差多少天,在折算成相差几年几月几日的

//计算两个时间的间隔
System.out.println("-------------------------------");
LocalDateTime date3 = LocalDateTime.now();
LocalDateTime date4 = LocalDateTime.of(2018, 1, 13, 22, 30, 10);
Duration duration = Duration.between(date3, date4);
System.out.println(date3 + " 与 " + date4 + " 间隔 " + "\n"
+ " 天 :" + duration.toDays() + "\n"
+ " 时 :" + duration.toHours() + "\n"
+ " 分 :" + duration.toMinutes() + "\n"
+ " 毫秒 :" + duration.toMillis() + "\n"
+ " 纳秒 :" + duration.toNanos() + "\n"
);
//注意,并没有获得秒差的,但既然可以获得毫秒,秒就可以自行获取了
相差年数 : 0
相差月数 : -11
相差日数 : -1
-------------------------------
相差的年月日分别为 : 0,-11,-1
-------------------------------
2021-04-26T21:08:30.340904400 与 2018-01-13T22:30:10 间隔
天 :-1198
时 :-28774
分 :-1726478
毫秒 :-103588700340
纳秒 :-103588700340904400

当计算程序的运行时间时,应当使用时间戳 Instant

Instant ins1 = Instant.now();
for (int i = 0; i < 10000000; i++) {
//循环一百万次
}

Instant ins2 = Instant.now();
Duration duration = Duration.between(ins1, ins2);
System.out.println("程序运行耗时为 : " + duration.toMillis() + "毫秒");

新的时间日期的格式化

使用自带的日期格式

返回的是 String

//使用jdk自身配置好的日期格式
DateTimeFormatter formatter1 = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime date1 = LocalDateTime.now();
//反过来调用也可以 : date1.format(formatter1);
String date1Str = formatter1.format(date1);
System.out.println(date1Str);
2021-04-26T21:10:13.530693

使用自定义格式

LocalDateTime date1 = LocalDateTime.now();
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String date2Str = formatter2.format(date1);
System.out.println(date2Str);

运行结果:

2021年04月26日 21:10:56

注:自定义转化的格式一定要与日期类型对应

  • LocalDate 只能设置仅含年月日的格式
  • LocalTime 只能设置仅含时分秒的格式
  • LocalDateTime 可以设置含年月日时分秒的格式

代码如下:

DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
System.out.println(formatter3.format(LocalDate.now()));

System.out.println("-------------------------------");

DateTimeFormatter formatter4 = DateTimeFormatter.ofPattern("HH:mm:ss");
System.out.println(formatter4.format(LocalTime.now()));

运行结果:

2021-04-26
-------------------------------
21:12:18

将时间字符串形式转化为日期对象

String datetime =  "2018-01-13 21:27:30";  
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse(datetime, dtf);
System.out.println(ldt);

运行结果:

2018-01-13T21:27:30

注:格式的写法必须与字符串的形式一样

  • 2018-01-13 21:27:30 对应 yyyy-MM-dd HH:mm:ss
  • 20180113213328 对应 yyyyMMddHHmmss

否则会报运行时异常!

但要记住:得到的最终结果都是类似 2018-01-13T21:27:30 的格式

因为在输出 LocalDateTime 对象时,会调用其重写的 toString 方法。

@Override
public String toString() {
return date.toString() + 'T' + time.toString();
}

将时间日期对象转为格式化后的时间日期对象

//新的格式化API中,格式化后的结果都默认是String,有时我们也需要返回经过格式化的同类型对象
LocalDateTime ldt1 = LocalDateTime.now();
DateTimeFormatter dtf1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String temp = dtf1.format(ldt1);
LocalDateTime formatedDateTime = LocalDateTime.parse(temp, dtf1);
System.out.println(formatedDateTime);

输出结果为:

2021-04-26T21:15:12

long 毫秒值转换为日期

System.out.println("---------long毫秒值转换为日期---------");
DateTimeFormatter df= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String longToDateTime = df.format(LocalDateTime.ofInstant(
Instant.ofEpochMilli(System.currentTimeMillis()),ZoneId.of("Asia/Shanghai")));

System.out.println(longToDateTime);

运行结果:

---------long毫秒值转换为日期---------
2021-04-26 21:16:13

Reference

参考资料 SimpleDateFormat线程不安全及解决办法 参考资料 LocalDateTime用法